// 版权所有2012 Go作者。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// cookiejar包实现了一个与内存中RFC 6265兼容的http。CookieJar。
package cookiejar

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/internal/ascii"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"
)

// PublicSuffixList提供域的公共后缀。例如：“example.com”的公共后缀是“com”，而“foo1.foo2.foo3.co.uk”的公共后缀是“co.uk”，而“bar.pvt.k12.ma.us”的公共后缀是“pvt.k12.ma.us”。
// 
// PublicSuffixList的实现必须是安全的，以供多个goroutine并发使用。
// 
// 始终返回“”的实现是有效的，可能对
// 测试有用，但它不安全：这意味着用于foo的HTTP服务器。com可以为bar设置cookie。通用域名格式。
// 
// 包
// golang中有一个公共后缀列表实现。org/x/net/publicsuffix。
type PublicSuffixList interface {
	// PublicSuffix返回域的公共后缀。
	// 
	// TODO:指定调用方和被调用方中的哪一方负责IP 
	// 地址、前导点和尾随点、区分大小写，以及
	// IDN/Punycode。
	PublicSuffix(domain string) string

	// 字符串返回此公共后缀
	// 列表的源描述。描述通常会包含时间
	// 戳或版本号之类的内容。
	String() string
}

// 选项是创建新Jar的选项。
type Options struct {
	// PublicSuffixList是公共后缀列表，用于确定HTTP服务器是否可以为域设置cookie。
	// 
	// 一个空值是有效的，可能对测试有用，但它不是
	// 安全的：它意味着用于foo的HTTP服务器。co.uk可以为吧台设置cookie 
	// 。co.uk。
	PublicSuffixList PublicSuffixList
}

// Jar实现了http。net/http包中的CookieJar接口。
type Jar struct {
	psList PublicSuffixList

	// mu锁定剩余字段。
	mu sync.Mutex

	// 条目是一组条目，由它们的eTLD+1键控，并由
	// 子键控它们的名称/域/路径。
	entries map[string]map[string]entry

	// nextSeqNum是分配给新cookie的下一个序列号
	// created SetCookies。
	nextSeqNum uint64
}

// New返回一个新的饼干罐。零*期权相当于零
// 期权。
func New(o *Options) (*Jar, error) {
	jar := &Jar{
		entries: make(map[string]map[string]entry),
	}
	if o != nil {
		jar.psList = o.PublicSuffixList
	}
	return jar, nil
}

// 条目是cookie的内部表示形式。
// 
// 此结构类型不在此包之外使用，但导出的
// 字段是RFC 6265的字段。
type entry struct {
	Name       string
	Value      string
	Domain     string
	Path       string
	SameSite   string
	Secure     bool
	HttpOnly   bool
	Persistent bool
	HostOnly   bool
	Expires    time.Time
	Creation   time.Time
	LastAccess time.Time

	// seqNum是一个序列号，因此Cookie以确定的顺序返回Cookie，即使对于路径长度和创建时间相同的Cookie也是如此。这简化了测试。
	seqNum uint64
}

// id返回域；路径将e的三元组命名为id。
func (e *entry) id() string {
	return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
}

// shouldSend确定e的cookie是否符合包含在
// host/path请求中的条件。呼叫方有责任检查
// cookie是否过期。
func (e *entry) shouldSend(https bool, host, path string) bool {
	return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
}

// domainMatch实现RFC 6265第5.1.3节的“域匹配”。
func (e *entry) domainMatch(host string) bool {
	if e.Domain == host {
		return true
	}
	return !e.HostOnly && hasDotSuffix(host, e.Domain)
}

// pathMatch根据RFC 6265第5.1.4节实现“路径匹配”。
func (e *entry) pathMatch(requestPath string) bool {
	if requestPath == e.Path {
		return true
	}
	if strings.HasPrefix(requestPath, e.Path) {
		if e.Path[len(e.Path)-1] == '/' {
			return true // “/any/”匹配“/any/path”案例。
		} else if requestPath[len(e.Path)] == '/' {
			return true // “/any”匹配“/any/path”案例。
		}
	}
	return false
}

// hasDotSuffix报告s是否以“.”结尾+后缀
func hasDotSuffix(s, suffix string) bool {
	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
}

// Cookies实现了http的Cookies方法。CookieJar接口。
// 
// 如果URL的方案不是HTTP或HTTPS，则返回空切片。
func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
	return j.cookies(u, time.Now())
}

// cookies与cookies类似，但以当前时间为参数。
func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
	if u.Scheme != "http" && u.Scheme != "https" {
		return cookies
	}
	host, err := canonicalHost(u.Host)
	if err != nil {
		return cookies
	}
	key := jarKey(host, j.psList)

	j.mu.Lock()
	defer j.mu.Unlock()

	submap := j.entries[key]
	if submap == nil {
		return cookies
	}

	https := u.Scheme == "https"
	path := u.Path
	if path == "" {
		path = "/"
	}

	modified := false
	var selected []entry
	for id, e := range submap {
		if e.Persistent && !e.Expires.After(now) {
			delete(submap, id)
			modified = true
			continue
		}
		if !e.shouldSend(https, host, path) {
			continue
		}
		e.LastAccess = now
		submap[id] = e
		selected = append(selected, e)
		modified = true
	}
	if modified {
		if len(submap) == 0 {
			delete(j.entries, key)
		} else {
			j.entries[key] = submap
		}
	}

	// 根据RFC 6265第5.4节第2点：按最长
	// 路径排序，然后按最早创建时间排序。
	sort.Slice(selected, func(i, j int) bool {
		s := selected
		if len(s[i].Path) != len(s[j].Path) {
			return len(s[i].Path) > len(s[j].Path)
		}
		if !s[i].Creation.Equal(s[j].Creation) {
			return s[i].Creation.Before(s[j].Creation)
		}
		return s[i].seqNum < s[j].seqNum
	})
	for _, e := range selected {
		cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
	}

	return cookies
}

// SetCookies实现http的SetCookies方法。CookieJar接口。
// 
// 如果URL的方案不是HTTP或HTTPS，它什么也不做。
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
	j.setCookies(u, cookies, time.Now())
}

// setCookies与setCookies类似，但以当前时间为参数。
func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
	if len(cookies) == 0 {
		return
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return
	}
	host, err := canonicalHost(u.Host)
	if err != nil {
		return
	}
	key := jarKey(host, j.psList)
	defPath := defaultPath(u.Path)

	j.mu.Lock()
	defer j.mu.Unlock()

	submap := j.entries[key]

	modified := false
	for _, cookie := range cookies {
		e, remove, err := j.newEntry(cookie, now, defPath, host)
		if err != nil {
			continue
		}
		id := e.id()
		if remove {
			if submap != nil {
				if _, ok := submap[id]; ok {
					delete(submap, id)
					modified = true
				}
			}
			continue
		}
		if submap == nil {
			submap = make(map[string]entry)
		}

		if old, ok := submap[id]; ok {
			e.Creation = old.Creation
			e.seqNum = old.seqNum
		} else {
			e.Creation = now
			e.seqNum = j.nextSeqNum
			j.nextSeqNum++
		}
		e.LastAccess = now
		submap[id] = e
		modified = true
	}

	if modified {
		if len(submap) == 0 {
			delete(j.entries, key)
		} else {
			j.entries[key] = submap
		}
	}
}

// 如果存在，canonicalHost会从主机上剥离端口，并返回规范化的
// 主机名。
func canonicalHost(host string) (string, error) {
	var err error
	if hasPort(host) {
		host, _, err = net.SplitHostPort(host)
		if err != nil {
			return "", err
		}
	}
	if strings.HasSuffix(host, ".") {
		// 从完全限定的域名中去掉尾随的点。
		host = host[:len(host)-1]
	}
	encoded, err := toASCII(host)
	if err != nil {
		return "", err
	}
	// 我们知道这是ascii码，无需检查。
	lower, _ := ascii.ToLower(encoded)
	return lower, nil
}

// hasPort报告主机是否包含端口号。主机可以是主机
// 名称、IPv4或IPv6地址。
func hasPort(host string) bool {
	colons := strings.Count(host, ":")
	if colons == 0 {
		return false
	}
	if colons == 1 {
		return true
	}
	return host[0] == '[' && strings.Contains(host, "]:")
}

// jarKey返回用于jar的密钥。
func jarKey(host string, psl PublicSuffixList) string {
	if isIP(host) {
		return host
	}

	var i int
	if psl == nil {
		i = strings.LastIndex(host, ".")
		if i <= 0 {
			return host
		}
	} else {
		suffix := psl.PublicSuffix(host)
		if suffix == host {
			return host
		}
		i = len(host) - len(suffix)
		if i <= 0 || host[i-1] != '.' {
			// 提供的公共后缀列表psl已损坏。
			// 在主机下存储cookie是一种安全的权宜之计。
			return host
		}
		// 这里只使用len（后缀）来确定来自
		// 的jar密钥，所以如果psl是正确的。PublicSuffix（“www.buggy.psl”）
		// 在从主机生成jar密钥时返回“com”。
	}
	prevDot := strings.LastIndex(host[:i-1], ".")
	return host[prevDot+1:]
}

// isIP报告主机是否为IP地址。
func isIP(host string) bool {
	return net.ParseIP(host) != nil
}

// defaultPath根据
// RFC 6265第5.1.4节返回URL路径的目录部分。
func defaultPath(path string) string {
	if len(path) == 0 || path[0] != '/' {
		return "/" // 路径为空或格式不正确。
	}

	i := strings.LastIndex(path, "/") // 路径以“/”开头，所以我！=-1.
	if i == 0 {
		return "/" // 路径的形式为“/abc”。
	}
	return path[:i] // 路径的格式为“/abc/xyz”或“/abc/xyz/”。
}

// newEntry从http创建一个条目。Cookie c.now是当前时间，将
// 与c进行比较。Expires用于确定是否删除c.defPath和host是
// 默认路径，以及从中接收到的URL c的规范主机名。
// 
// 删除jar是否应该删除此cookie的记录，因为它已经过期。在这种情况下，e可能不完整，但
// 调用e.id是有效的（这取决于e的名称、域和路径）。
// 
// 格式错误的c.域将导致错误。
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
	e.Name = c.Name

	if c.Path == "" || c.Path[0] != '/' {
		e.Path = defPath
	} else {
		e.Path = c.Path
	}

	e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
	if err != nil {
		return e, false, err
	}

	// MaxAge优先于Expires。
	if c.MaxAge < 0 {
		return e, true, nil
	} else if c.MaxAge > 0 {
		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
		e.Persistent = true
	} else {
		if c.Expires.IsZero() {
			e.Expires = endOfTime
			e.Persistent = false
		} else {
			if !c.Expires.After(now) {
				return e, true, nil
			}
			e.Expires = c.Expires
			e.Persistent = true
		}
	}

	e.Value = c.Value
	e.Secure = c.Secure
	e.HttpOnly = c.HttpOnly

	switch c.SameSite {
	case http.SameSiteDefaultMode:
		e.SameSite = "SameSite"
	case http.SameSiteStrictMode:
		e.SameSite = "SameSite=Strict"
	case http.SameSiteLaxMode:
		e.SameSite = "SameSite=Lax"
	}

	return e, false, nil
}

var (
	errIllegalDomain   = errors.New("cookiejar: illegal cookie domain attribute")
	errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
	errNoHostname      = errors.New("cookiejar: no host name available (IP only)")
)

// endOfTime是会话（非持久性）cookie过期的时间。
// 这个瞬间在大多数日期/时间格式（不仅仅是
// Go’s time.time）中都是可以表示的，而且在将来应该足够了。
var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)

// domainAndType确定cookie的domain和hostOnly属性。
func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
	if domain == "" {
		// SetCookie头中没有域属性表示
		// 主机cookie。
		return host, true, nil
	}

	if isIP(host) {
		// 根据RFC 6265，域匹配包括不被
		// IP地址。
		// TODO:这可能会像在普通浏览器中一样轻松。
		return "", false, errNoHostname
	}

	// 从这里开始：如果cookie有效，则它是一个域cookie（带有
	// 下面一个公共后缀的例外）。
	// 参见RFC 6265第5.2.3节。
	if domain[0] == '.' {
		domain = domain[1:]
	}

	if len(domain) == 0 || domain[0] == '.' {
		// 收到“域=”或者“Domain=…some.thing”，
		// 两者都是非法的。
		return "", false, errMalformedDomain
	}

	domain, isASCII := ascii.ToLower(domain)
	if !isASCII {
		// 收到的是非ASCII域，例如“perché.com”而不是“xn--perch fsa.com”
		return "", false, errMalformedDomain
	}

	if domain[len(domain)-1] == '.' {
		// 我们收到了类似“domain=www.example.com”的内容。
		// 浏览器确实可以处理此类内容（实际上是不同的），但
		// RFC 6265似乎在
		// 中明确要求拒绝（例如第4.1.2.3节）。4.1.2.3不规范，但
		// /“域名匹配”（5.1.3）和“规范化主机名”
		// /（5.1.2）不规范。
		return "", false, errMalformedDomain
	}

	// 参见RFC 6265第5.3#5节。
	if j.psList != nil {
		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
			if host == domain {
				// 这是一个例外，其中带有域属性的cookie 
				// 是主机cookie。
				return host, true, nil
			}
			return "", false, errIllegalDomain
		}
	}

	// 域必须与主机匹配：www.mycompany。com无法
	// 为设置cookies。我们的竞争对手。通用域名格式。
	if host != domain && !hasDotSuffix(host, domain) {
		return "", false, errIllegalDomain
	}

	return domain, false, nil
}
